#/usr/bin/env python
# Wed Jul 22 22:27:15 CET 2015  Sergio Galisteo <http://www.elblogdigital.com>
# iOS parse manifest python script
#
# Create the file structure of iOS devices from iTunes Backups parsing
# the Manifest.mbdb file. Useful for forensics analysis of iOS devices.
#
# Usage: python iOS_parse_Manifest.py [Path to Manifest.mbdb file] [Output path]
# Example: 
# python iOS_parse_Manifest.py /home/macuser/Library/Application Support/MobileSync/Backup/e1819649a37e0238ad554359b893f013a466be8d/ ../output
# 
# Trick: Copy the script into the backup folder and execute with relative paths.
# Example: python iOS_parse_Manifest.py ./ ../output
# 
# Some notes:
# 2 Required parameters: Path of Manifest.mbdb and output path
# The output folder will be the output path + the uid of the backup + the string '_parsed'
# If the output folder already exists will not continue.
#
# The functions to read the mbdb database are extracted from:
# http://stackoverflow.com/questions/3085153/how-to-parse-the-manifest-mbdb-file-in-an-ios-4-0-itunes-backup

from shutil import *
import os, sys
import hashlib

# Required parameters: Path of Manifest.mbdb and output path
if len(sys.argv)!=3:
	print "\nUsage: python iOS_parse_Manifest.py [Path to Manifest.mbdb file] [Output path]"
	print "Example: python iOS_parse_Manifest.py ./ ../output"
	sys.exit(0)
mbdb_path=sys.argv[1]
output_path=sys.argv[2]

mbdx = {}

def getint(data, offset, intsize):
    """Retrieve an integer (big-endian) and new offset from the current offset"""
    value = 0
    while intsize > 0:
        value = (value<<8) + ord(data[offset])
        offset = offset + 1
        intsize = intsize - 1
    return value, offset

def getstring(data, offset):
    """Retrieve a string and new offset from the current offset into the data"""
    if data[offset] == chr(0xFF) and data[offset+1] == chr(0xFF):
        return '', offset+2 # Blank string
    length, offset = getint(data, offset, 2) # 2-byte length
    value = data[offset:offset+length]
    return value, (offset + length)

def process_mbdb_file(filename):
    mbdb = {} # Map offset of info in this file => file info
    data = open(filename).read()
    if data[0:4] != "mbdb": raise Exception("This does not look like an MBDB file")
    offset = 4
    offset = offset + 2 # value x05 x00, not sure what this is
    while offset < len(data):
        fileinfo = {}
        fileinfo['start_offset'] = offset
        fileinfo['domain'], offset = getstring(data, offset)
        fileinfo['filename'], offset = getstring(data, offset)
        fileinfo['linktarget'], offset = getstring(data, offset)
        fileinfo['datahash'], offset = getstring(data, offset)
        fileinfo['unknown1'], offset = getstring(data, offset)
        fileinfo['mode'], offset = getint(data, offset, 2)
        fileinfo['unknown2'], offset = getint(data, offset, 4)
        fileinfo['unknown3'], offset = getint(data, offset, 4)
        fileinfo['userid'], offset = getint(data, offset, 4)
        fileinfo['groupid'], offset = getint(data, offset, 4)
        fileinfo['mtime'], offset = getint(data, offset, 4)
        fileinfo['atime'], offset = getint(data, offset, 4)
        fileinfo['ctime'], offset = getint(data, offset, 4)
        fileinfo['filelen'], offset = getint(data, offset, 8)
        fileinfo['flag'], offset = getint(data, offset, 1)
        fileinfo['numprops'], offset = getint(data, offset, 1)
        fileinfo['properties'] = {}
        for ii in range(fileinfo['numprops']):
            propname, offset = getstring(data, offset)
            propval, offset = getstring(data, offset)
            fileinfo['properties'][propname] = propval
        mbdb[fileinfo['start_offset']] = fileinfo
        fullpath = fileinfo['domain'] + '-' + fileinfo['filename']
        id = hashlib.sha1(fullpath)
        mbdx[fileinfo['start_offset']] = id.hexdigest()
    return mbdb

def create_file(fileid, filename):
	# First, we create the directory structure
	if not os.path.exists(os.path.dirname(filename)):
		os.makedirs(os.path.dirname(filename))
		
	try:
		copy2(fileid, filename)
	except OSError as why:
		print fileid+' '+filename+' '+str(why)
	except Error as err:
		print err.args[0]
	except IOError as err:
		print 'Error copying '+fileid+' to '+filename

	return
	
if __name__ == '__main__':
	# Checking if the Manifest.mbdb exists in the path
	if not os.path.exists(mbdb_path + "/Manifest.mbdb"):
		print "ERROR: Manifest.mbdb not found"
		sys.exit(0)

	# The output folder will be the output path + the uid of the backup + the string _parsed
	output_path=os.path.realpath(output_path)+'/'+os.path.basename(os.path.realpath(mbdb_path))+'_parsed'
	
	# If the output folder already exists will not continue
	if os.path.exists(output_path):
		print 'ERROR: The output folder '+output_path+' already exists. Please select another path or delete the current folder.'
		sys.exit(0)
	
	# We create the output folder
	os.makedirs(output_path)
	
	mbdb = process_mbdb_file(mbdb_path+"Manifest.mbdb")
	
	for offset, fileinfo in mbdb.items():
		if offset in mbdx:
			fileinfo['fileID'] = mbdx[offset]
		else:
			fileinfo['fileID'] = "<nofileID>"
			print >> sys.stderr, "No fileID found for %s" % fileinfo_str(fileinfo)
		
		if os.path.exists(mbdb_path + "/" + fileinfo['fileID']):
			print 'Copying '+fileinfo['fileID']+' to '+fileinfo['filename']
			create_file(mbdb_path + "/" + fileinfo['fileID'],output_path + "/" + fileinfo['filename'])
